package com.czg.jsonschema2lombokpojo.rules;

import static java.util.Arrays.*;
import static org.apache.commons.lang3.StringUtils.*;
import static com.czg.jsonschema2lombokpojo.rules.PrimitiveTypes.*;
import static com.czg.jsonschema2lombokpojo.util.TypeUtil.*;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import com.czg.jsonschema2lombokpojo.JMyStringLiteral;
import com.czg.jsonschema2lombokpojo.annotator.Annotator;
import com.czg.jsonschema2lombokpojo.RuleLogger;
import com.czg.jsonschema2lombokpojo.Schema;
import com.czg.jsonschema2lombokpojo.exception.ClassAlreadyExistsException;
import com.czg.jsonschema2lombokpojo.exception.GenerationException;
import com.czg.jsonschema2lombokpojo.model.EnumDefinition;
import com.czg.jsonschema2lombokpojo.model.EnumDefinitionExtensionType;
import com.czg.jsonschema2lombokpojo.model.EnumValueDefinition;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.sun.codemodel.*;

/**
 * 应用“enum”schema规则。
 *
 * @see <a href=
 * "http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.19">http:
 * //tools.ietf.org/html/draft-zyp-json-schema-03#section-5.19</a>
 */
public class EnumRule implements Rule<JClassContainer, JType> {

    private static final String VALUE_FIELD_NAME = "value";
    private static final String DESC_FIELD_NAME = "desc";

    private final RuleFactory ruleFactory;

    protected EnumRule(RuleFactory ruleFactory) {
        this.ruleFactory = ruleFactory;
    }

    /**
     * 应用此schema规则执行所需的代码生成步骤。
     * <p>
     * 创建一个Java {@link Enum}，并为schema中存在的每个枚举值添加常量。枚举名称派生自nodeName，枚举类型本身被创建为所属类型的内部类。
     * 在很少的情况下，不存在拥有类型（枚举是schema的根），那么枚举就会成为一个独立的公共类。
     * <p>
     * 每个枚举常量的实际JSON值保存在生成类型中名为“value”的属性中。
     * 静态工厂方法<code>fromValue(String)</code>被添加到生成的enum中，并且这些方法被注解以允许Jackson正确地封送/反封送值。
     *
     * @param nodeName  属性的名称是一个“enum”
     * @param node      枚举节点
     * @param parent    父节点
     * @param container 要向其添加此enum的类容器（类或包）
     * @return 新生成的Java类型，用于表示给定的枚举
     */
    @Override
    public JType apply(String nodeName, JsonNode node, JsonNode parent, JClassContainer container, Schema schema) {
        Annotator annotator = ruleFactory.getAnnotator();
        if (node.has("existingJavaType")) {
            JType type = ruleFactory.getTypeRule().apply(nodeName, node, parent, container.getPackage(), schema);
            schema.setJavaTypeIfEmpty(type);
            return type;
        }

        JDefinedClass _enum;
        try {
            _enum = createEnum(node, nodeName, container);
        } catch (ClassAlreadyExistsException e) {
            return e.getExistingClass();
        }

        schema.setJavaTypeIfEmpty(_enum);

        // Add JavaDocs
        if (node.has("title")) {
            ruleFactory.getTitleRule().apply(nodeName, node.get("title"), node, _enum, schema);
        }

        if (node.has("description")) {
            ruleFactory.getDescriptionRule().apply(nodeName, node.get("description"), node, _enum, schema);
        }

        if (node.has("$comment")) {
            ruleFactory.getCommentRule().apply(nodeName, node.get("$comment"), node, _enum, schema);
        }

        if (node.has("javaInterfaces")) {
            addInterfaces(_enum, node.get("javaInterfaces"));
        }

        // copy our node; remove the javaType as it will throw off the TypeRule for our case
        ObjectNode typeNode = node.deepCopy();
        typeNode.remove("javaType");

        // 如果在枚举上指定了type，则获取它的类型规则。否则，我们就是一个字符串。
        // （这与Object的默认值不同，这就是为什么我们不是在每种情况下都这样做。）
        JType backingType = node.has("type") ?
                ruleFactory.getTypeRule().apply(nodeName, typeNode, parent, container, schema) :
                container.owner().ref(String.class);

        EnumDefinition enumDefinition = buildEnumDefinition(nodeName, node, backingType);

        JFieldVar valueField = addConstructorAndValueFields(enumDefinition, _enum);
        JFieldVar descField = addConstructorAndDescFields(enumDefinition, _enum);

        // 只有当我们有一个合理的字符串返回时，才重写toString
        if (isString(backingType)) {
            addToString(_enum, valueField);
        }

        JMethod valueMethod = addFieldAccessors(_enum, valueField, "value");
        DescriptionRule.addDoc(valueMethod, "获得枚举值");
        JMethod descMethod = addFieldAccessors(_enum, descField, "desc");
        DescriptionRule.addDoc(descMethod, "获得枚举描述");
        addEnumConstants(enumDefinition, _enum, schema);
        JMethod fromValueMethod = addFactoryMethod(enumDefinition, _enum);
        DescriptionRule.addDoc(fromValueMethod, "value获得枚举类型");
        applyCustomizations(enumDefinition, _enum);
        annotator.typeInfo(_enum, node,true);
        return _enum;
    }

    /**
     * 增加枚举常量内容
     *
     * @param enumDefinition
     * @param _enum
     * @param schema
     */
    protected void addEnumConstants(EnumDefinition enumDefinition, JDefinedClass _enum, Schema schema) {

        JType type = enumDefinition.getBackingType();

        String nodeName = enumDefinition.getNodeName();
        JsonNode parentNode = enumDefinition.getEnumNode();

        for (EnumValueDefinition enumValueDefinition : enumDefinition.values()) {

            JEnumConstant constant = _enum.enumConstant(enumValueDefinition.getName());
            String value = enumValueDefinition.getValue();
            String desc = enumValueDefinition.getDesc();
            constant.arg(DefaultRule.getDefaultValue(type, value));
            constant.arg(new JMyStringLiteral(desc));
            Annotator annotator = ruleFactory.getAnnotator();
            annotator.enumConstant(_enum, constant, value);

            String enumNodeName = nodeName + "#" + value;

            if (enumValueDefinition.hasTitle()) {
                JsonNode titleNode = enumValueDefinition.getTitleNode();
                ruleFactory.getTitleRule().apply(enumNodeName, titleNode, parentNode, constant, schema);
            }

            if (enumValueDefinition.hasDescription()) {
                JsonNode descriptionNode = enumValueDefinition.getDescriptionNode();
                ruleFactory.getDescriptionRule().apply(enumNodeName, descriptionNode, parentNode, constant, schema);
            }
        }
    }

    /**
     * 允许自定义{@link EnumRule}实现扩展{@link EnumRule}并做一些自定义行为。
     * <p>
     * 此方法是专门添加的，因此自定义枚举规则开发人员不需要覆盖apply方法。
     *
     * @param enumDefinition 枚举定义。
     * @param _enum          生成的类模型
     */
    protected void applyCustomizations(EnumDefinition enumDefinition, JDefinedClass _enum) {
        // 在这里自定义枚举规则实现的，暂时还没有需要自定义的东西。
    }

    /**
     * 根据所提供的schema元素构建枚举的有效定义。
     * <p>
     * 这个函数决定了它应该将定义的创建委托给哪个方法：
     * <p>
     * 对于“enum”由{@link #buildEnumDefinitionWithNoExtensions(String, JsonNode, JsonNode, JType)}处理的“enum”
     * 对于“enum”和“javaEnumComments”由{@link #buildEnumDefinitionWithJavaEnumCommentsExtension(String, JsonNode, JsonNode, JsonNode, JsonNode, JsonNode, JType)}处理
     * 对于“enum”和“javaEnumNames”由{@link #buildEnumDefinitionWithJavaEnumNamesExtension(String, JsonNode, JsonNode, JsonNode, JType)}处理
     *
     * @param nodeName    属性的名称是一个“enum”
     * @param node        枚举节点
     * @param backingType 支持enum值的对象，最常见的是字符串
     * @return 枚举的有效定义
     */
    protected EnumDefinition buildEnumDefinition(String nodeName, JsonNode node, JType backingType) {

        JsonNode enums = node.path("enum");
        JsonNode javaEnumComments = node.path("javaEnumComments");
        JsonNode javaEnumNames = node.path("javaEnumNames");
        JsonNode javaEnumDescriptions = node.path("javaEnumDescriptions");
        RuleLogger logger = ruleFactory.getLogger();

        EnumDefinition enumDefinition;
        if (!javaEnumComments.isMissingNode()) {
            enumDefinition = buildEnumDefinitionWithJavaEnumCommentsExtension(nodeName, node, enums, javaEnumNames, javaEnumDescriptions, javaEnumComments, backingType);
        } else if (!javaEnumNames.isMissingNode()) {
            if (!javaEnumDescriptions.isMissingNode()) {
                enumDefinition = buildEnumDefinitionWithJavaEnumNamesAndJavaEnumDescriptionsExtension(nodeName, node, enums, javaEnumNames, javaEnumDescriptions, backingType);
            } else {
                enumDefinition = buildEnumDefinitionWithJavaEnumNamesExtension(nodeName, node, enums, javaEnumNames, backingType);
            }
        } else {
            enumDefinition = buildEnumDefinitionWithNoExtensions(nodeName, node, enums, backingType);
        }
        return enumDefinition;
    }

    /**
     * 不存在扩展时，构建枚举定义
     *
     * @param nodeName
     * @param parentNode
     * @param enums
     * @param backingType
     * @return
     */
    protected EnumDefinition buildEnumDefinitionWithNoExtensions(String nodeName, JsonNode parentNode, JsonNode enums, JType backingType) {
        ArrayList<EnumValueDefinition> enumValues = new ArrayList<>();

        Collection<String> existingConstantNames = new ArrayList<>();

        for (int i = 0; i < enums.size(); i++) {
            JsonNode value = enums.path(i);

            if (!value.isNull()) {
                String constantName = getConstantName(value.asText(), null);
                constantName = makeUnique(constantName, existingConstantNames);
                existingConstantNames.add(constantName);

                enumValues.add(new EnumValueDefinition(constantName, value.asText(), constantName));
            }
        }

        return new EnumDefinition(nodeName, parentNode, backingType, enumValues, EnumDefinitionExtensionType.NONE);
    }

    /**
     * 存在javaEnumNames扩展时，构建枚举定义
     *
     * @param nodeName
     * @param parentNode
     * @param enums
     * @param javaEnumNames
     * @param backingType
     * @return
     */
    protected EnumDefinition buildEnumDefinitionWithJavaEnumNamesExtension(String nodeName, JsonNode parentNode, JsonNode enums, JsonNode javaEnumNames, JType backingType) {
        ArrayList<EnumValueDefinition> enumValues = new ArrayList<>();
        Collection<String> existingConstantNames = new ArrayList<>();
        for (int i = 0; i < enums.size(); i++) {
            JsonNode value = enums.path(i);
            if (!value.isNull()) {
                String nameText = javaEnumNames.path(i).asText();
                String constantName = getConstantName(value.asText(), nameText);
                constantName = makeUnique(constantName, existingConstantNames);
                existingConstantNames.add(constantName);
                enumValues.add(new EnumValueDefinition(constantName, value.asText(), nameText, javaEnumNames));
            }
        }
        return new EnumDefinition(nodeName, parentNode, backingType, enumValues, EnumDefinitionExtensionType.JAVA_ENUM_NAMES);
    }

    protected EnumDefinition buildEnumDefinitionWithJavaEnumNamesAndJavaEnumDescriptionsExtension(String nodeName, JsonNode parentNode, JsonNode enums, JsonNode javaEnumNames, JsonNode javaEnumDescriptions, JType backingType) {
        ArrayList<EnumValueDefinition> enumValues = new ArrayList<>();
        Collection<String> existingConstantNames = new ArrayList<>();
        for (int i = 0; i < enums.size(); i++) {
            JsonNode value = enums.path(i);
            if (!value.isNull()) {
                String constantName = getConstantName(value.asText(), javaEnumNames.path(i).asText());
                String descName = javaEnumDescriptions.path(i).asText();
                constantName = makeUnique(constantName, existingConstantNames);
                existingConstantNames.add(constantName);
                enumValues.add(new EnumValueDefinition(constantName, value.asText(), descName, javaEnumNames));
            }
        }
        return new EnumDefinition(nodeName, parentNode, backingType, enumValues, EnumDefinitionExtensionType.JAVA_ENUM_NAMES);
    }

    /**
     * @param nodeName
     * @param enumNode
     * @param enums
     * @param javaEnumComments
     * @param type
     * @return
     */
    protected EnumDefinition buildEnumDefinitionWithJavaEnumCommentsExtension(String nodeName, JsonNode enumNode, JsonNode enums, JsonNode javaEnumNames, JsonNode javaEnumDescriptions, JsonNode javaEnumComments, JType type) {
        ArrayList<EnumValueDefinition> enumValues = new ArrayList<>();
        Collection<String> existingConstantNames = new ArrayList<>();
        for (int i = 0; i < enums.size(); i++) {
            JsonNode value = enums.path(i);
            if (!value.isNull()) {
                JsonNode javaEnumNode = javaEnumComments.path(i);

                if (javaEnumNode.isMissingNode()) {
                    ruleFactory.getLogger().error("javaEnum entry for " + value.asText() + " was not found.");
                }
                String constantName = getConstantName(value.asText(), javaEnumNames.path(i).asText());
                String descName = "";
                if (!javaEnumDescriptions.isMissingNode()) {
                    descName = javaEnumDescriptions.path(i).asText();
                } else {
                    descName = value.asText();
                }

                constantName = makeUnique(constantName, existingConstantNames);
                existingConstantNames.add(constantName);

                JsonNode titleNode = javaEnumNode.path("title");
                JsonNode descriptionNode = javaEnumNode.path("description");

                enumValues.add(new EnumValueDefinition(constantName, value.asText(), descName, javaEnumNode, titleNode, descriptionNode));
            }
        }

        return new EnumDefinition(nodeName, enumNode, type, enumValues, EnumDefinitionExtensionType.JAVA_ENUMS);
    }

    protected JDefinedClass createEnum(JsonNode node, String nodeName, JClassContainer container) throws ClassAlreadyExistsException {

        try {
            if (node.has("javaType")) {
                String fqn = node.get("javaType").asText();

                if (isPrimitive(fqn, container.owner())) {
                    throw new GenerationException("Primitive type '" + fqn + "' cannot be used as an enum.");
                }

                if (fqn.lastIndexOf(".") == -1) { // not a fully qualified name
                    fqn = container.getPackage().name() + "." + fqn;
                }

                try {
                    Class<?> existingClass = Thread.currentThread().getContextClassLoader().loadClass(fqn);
                    throw new ClassAlreadyExistsException(container.owner().ref(existingClass));
                } catch (ClassNotFoundException e) {
                    JDefinedClass enumClass = container.owner()._class(fqn, ClassType.ENUM);
                    ruleFactory.getLogger().debug("Adding " + enumClass.fullName());
                    return enumClass;
                }
            } else {
                try {
                    JDefinedClass enumClass = container._class(JMod.PUBLIC, getEnumName(nodeName, node, container), ClassType.ENUM);
                    ruleFactory.getLogger().debug("Adding " + enumClass.fullName());
                    return enumClass;
                } catch (JClassAlreadyExistsException e) {
                    throw new GenerationException(e);
                }
            }
        } catch (JClassAlreadyExistsException e) {
            throw new ClassAlreadyExistsException(e.getExistingClass());
        }
    }

    protected JFieldVar addConstructorAndValueFields(EnumDefinition enumDefinition, JDefinedClass _enum) {

        JType backingType = enumDefinition.getBackingType();
        JFieldVar valueField = _enum.field(JMod.PRIVATE | JMod.FINAL, backingType, VALUE_FIELD_NAME);
        /*
        JMethod constructor = _enum.constructor(JMod.NONE);
        JVar valueParam = constructor.param(backingType, VALUE_FIELD_NAME);
        JBlock body = constructor.body();
        body.assign(JExpr._this().ref(valueField), valueParam);
         */

        return valueField;
    }

    protected JFieldVar addConstructorAndDescFields(EnumDefinition enumDefinition, JDefinedClass _enum) {

        JType backingType = enumDefinition.getBackingType();
        JFieldVar descField = _enum.field(JMod.PRIVATE | JMod.FINAL, backingType, DESC_FIELD_NAME);
        /*
        JMethod constructor = _enum.constructor(JMod.NONE);
        JVar valueParam = constructor.param(backingType, DESC_FIELD_NAME);
        JBlock body = constructor.body();
        body.assign(JExpr._this().ref(descField), valueParam);
         */

        return descField;
    }

    protected JMethod addFactoryMethod(EnumDefinition enumDefinition, JDefinedClass _enum) {

        JType backingType = enumDefinition.getBackingType();
        JFieldVar quickLookupMap = addQuickLookupMap(enumDefinition, _enum);

        JMethod fromValue = _enum.method(JMod.PUBLIC | JMod.STATIC, _enum, "fromValue");
        JVar valueParam = fromValue.param(backingType, "value");

        JBlock body = fromValue.body();
        JVar constant = body.decl(_enum, "constant");
        constant.init(quickLookupMap.invoke("get").arg(valueParam));

        JConditional _if = body._if(constant.eq(JExpr._null()));

        JInvocation illegalArgumentException = JExpr._new(_enum.owner().ref(IllegalArgumentException.class));
        JExpression expr = valueParam;

        // if string no need to add ""
        if (!isString(backingType)) {
            expr = expr.plus(JExpr.lit(""));
        }

        illegalArgumentException.arg(expr);
        _if._then()._throw(illegalArgumentException);
        _if._else()._return(constant);
        return fromValue;
    }

    /**
     * 生成字段访问函数
     *
     * @param _enum
     * @param valueField
     */
    protected JMethod addFieldAccessors(JDefinedClass _enum, JFieldVar valueField, String name) {
        JMethod jMethod = _enum.method(JMod.PUBLIC, valueField.type(), name);
        JBlock body = jMethod.body();
        body._return(JExpr._this().ref(valueField));
        return jMethod;
    }


    /**
     * 生成字段
     *
     * @param _enum
     * @param valueField
     */
    protected JMethod addStaticFieldAccessors(JDefinedClass _enum, JFieldVar valueField, String name) {
        JMethod jMethod = _enum.method(JMod.PUBLIC | JMod.STATIC, valueField.type(), name);
        JBlock body = jMethod.body();
        body._return(JExpr._this().ref(valueField));
        return jMethod;
    }

    protected JFieldVar addQuickLookupMap(EnumDefinition enumDefinition, JDefinedClass _enum) {

        JType backingType = enumDefinition.getBackingType();

        JClass lookupType = _enum.owner().ref(Map.class).narrow(backingType.boxify(), _enum);
        JFieldVar lookupMap = _enum.field(JMod.PRIVATE | JMod.STATIC | JMod.FINAL, lookupType, "enumConstants");
        JClass lookupImplType = _enum.owner().ref(HashMap.class).narrow(backingType.boxify(), _enum);
        lookupMap.init(JExpr._new(lookupImplType));

        JFieldVar lookupValueList = getNewStaticListField(backingType, _enum, "enumValueList");
        JClass lookupValueImplType = _enum.owner().ref(ArrayList.class).narrow(backingType.boxify());
        lookupValueList.init(JExpr._new(lookupValueImplType));

        JFieldVar lookupDescList = getNewStaticListField(backingType, _enum, "enumDescList");
        JClass lookupDescImplType = _enum.owner().ref(ArrayList.class).narrow(backingType.boxify());
        lookupDescList.init(JExpr._new(lookupDescImplType));

        JForEach forEach = _enum.init().forEach(_enum, "c", JExpr.invoke("values"));
        JInvocation put = forEach.body().invoke(lookupMap, "put");
        put.arg(forEach.var().ref("value"));
        put.arg(forEach.var());

        JInvocation putValue = forEach.body().invoke(lookupValueList, "put");
        putValue.arg(forEach.var().ref("value"));

        JInvocation putDesc = forEach.body().invoke(lookupDescList, "put");
        putDesc.arg(forEach.var().ref("desc"));

        addStaticFieldAccessors(_enum, lookupValueList, "getValues");
        addStaticFieldAccessors(_enum, lookupDescList, "getDescList");

        return lookupMap;
    }

    private JFieldVar getNewStaticListField(JType backingType, JDefinedClass _enum, String name) {
        JClass lookupType = _enum.owner().ref(List.class).narrow(backingType.boxify());
        return _enum.field(JMod.PRIVATE | JMod.STATIC | JMod.FINAL, lookupType, name);

    }

    protected void addToString(JDefinedClass _enum, JFieldVar valueField) {
        JMethod toString = _enum.method(JMod.PUBLIC, String.class, "toString");
        JBlock body = toString.body();

        JExpression toReturn = JExpr._this().ref(valueField);
        if (!isString(valueField.type())) {
            toReturn = toReturn.plus(JExpr.lit(""));
        }

        body._return(toReturn);

        toString.annotate(Override.class);
    }

    protected boolean isString(JType type) {
        return type.fullName().equals(String.class.getName());
    }

    protected String getEnumName(String nodeName, JsonNode node, JClassContainer container) {
        String fieldName = ruleFactory.getNameHelper().getClassName(nodeName, node) + (ruleFactory.getGenerationConfig().isEnumTypeAddSuffix() ? "Enum" : "");
        String className = ruleFactory.getNameHelper().replaceIllegalCharacters(capitalize(fieldName));
        String normalizedName = ruleFactory.getNameHelper().normalizeName(className);

        Collection<String> existingClassNames = new ArrayList<>();
        for (Iterator<JDefinedClass> classes = container.classes(); classes.hasNext(); ) {
            existingClassNames.add(classes.next().name());
        }
        return makeUnique(normalizedName, existingClassNames);
    }

    protected String makeUnique(final String name, Collection<String> existingNames) {
        if (existingNames.contains(name)) {
            String newName = name + "_";
            ruleFactory.getLogger().warn("Enum name " + name + " already used; trying to replace it with " + newName);
            return makeUnique(newName, existingNames);
        }
        return name;
    }

    protected String getConstantName(String nodeName, String customName) {
        if (isNotBlank(customName)) {
            return customName;
        }
        List<String> enumNameGroups = new ArrayList<>(asList(splitByCharacterTypeCamelCase(nodeName)));
        String enumName = "";
        enumNameGroups.removeIf(s -> containsOnly(ruleFactory.getNameHelper().replaceIllegalCharacters(s), "_"));
        enumName = upperCase(join(enumNameGroups, "_"));
        if (isEmpty(enumName)) {
            enumName = "__EMPTY__";
        } else if (Character.isDigit(enumName.charAt(0))) {
            enumName = "_" + enumName;
        }

        return enumName;
    }

    protected void addInterfaces(JDefinedClass jclass, JsonNode javaInterfaces) {
        for (JsonNode i : javaInterfaces) {
            jclass._implements(resolveType(jclass._package(), i.asText()));
        }
    }

}
